#!/usr/bin/env python
#-*- coding: UTF-8 -*-
# application.py: Programming interface of task manager for application
#
# Copyright (C) 2008-2010 Hily Jiang <hilyjiang@gmail.com>
#
# This file is part of dtask.
#
# This software is provided 'as-is', without any express or implied
# warranty.  In no event will the authors be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
#    claim that you wrote the original software. If you use this software
#    in a product, an acknowledgment in the product documentation would be
#    appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
#    misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.

"""
Programming interface of task manager for application.
"""

import threading
import socket
import select
import cjson

from utils import CommandBuffer


CFG_APP_BUFSIZ = 1024
            

class Task(object):
    """Task class."""

    def __init__(self, taskServerIP, taskServerPort,
                 taskType, taskParams, taskTimeout=0,
                 appName='', appKey=''):
        self.taskServerIP = taskServerIP
        self.taskServerPort = taskServerPort
        self.taskType = taskType
        self.taskParams = taskParams
        self.taskTimeout = taskTimeout
        self.appName = appName
        self.appKey = appKey
        self.taskIdEvent = threading.Event()
        self.taskRsEvent = threading.Event()
        self.taskId = None
        self.result = None
        self.started = False
        self.timeouted = False
        self.sock = None

    def __connect(self):
        """Connect to task server."""
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        addr = (self.taskServerIP, self.taskServerPort)
        try:
            self.sock.connect(addr)
        except:
#            print 'Can not connect to server: ', addr
            self.started = False
            return False

        # if appName exists send appName and appKey to server
        if self.appName != '':
            self.sock.sendall('PASSPORT %s %s\n' % (self.appName, self.appKey))

        return True

    def close(self):
        """Close connection to server."""
        if self.sock:
            self.sock.close()
            self.sock = None

    def run(self):
        """Start task. On error it return False."""
        if self.started: return
        self.started = True

        self.aBuf = CommandBuffer()
        if not self.__connect():
            return False

        # start a new thread to receive task id
        self.idThread = threading.Thread(target=self.__recvThreadFunc)
        self.idThread.setDaemon(True)
        self.idThread.start()

        # send task info to server
        self.sock.sendall('TASK %s %d %s\n' % (self.taskType, self.taskTimeout,
                                               self.taskParams))

        return True

    def __cancelResult(self):
        """Cancel the thread waiting for task result."""
        self.taskIdEvent.set()
        self.taskRsEvent.set()
        self.sock.close()
        self.sock = None
        self.timeouted = True

    def getResult(self, timeout=30, fullInfo=False):
        """Get result from task server.

        Set fullInfo to True to get result dict with full infomation,
        including fields as dict keys below:
            * 'tasktype': task type
            * 'params':   task params
            * 'status':   task status
                  'returned': result has return to application
                  'failed': task that retries over the limit or timeout
            * 'retries':  retry times
            * 'appname':  application name
            * 'appip':    application ip
            * 'appport':  application port
            * 'appast':   application assign time
            * 'apprtt':   application return time
            * 'schname':  scheduler name
            * 'schip':    scheduler ip
            * 'schport':  scheduler port
            * 'schast':   scheduler assign time
            * 'schrtt':   scheduler return time
            * 'nodmip':   node manager ip
            * 'nodmport': node manager port
            * 'nodmast':  node manager assign time
            * 'nodmrtt':  node manager return time
            * 'nodname':  node name
            * 'nodip':    node ip
            * 'nodport':  node port
            * 'nodast':   node assign time
            * 'nodrtt':   node return time
            * 'result':   task result
        Or else, simply return a result string.
        On error it return False.
        """
        # timer to receive task result
        self.rsTimer = threading.Timer(timeout, self.__cancelResult)
        self.rsTimer.start()

        if self.taskId == None:
            if not self.started: return False

            self.taskIdEvent.wait()
            self.taskIdEvent.clear()

            # timeout or error when return task id
            if self.taskId == None: return False
        else:
            self.taskIdEvent.clear()

        # start a new thread to receive result
        self.rsThread = threading.Thread(target=self.__recvThreadFunc)
        self.rsThread.setDaemon(True)
        self.rsThread.start()

        getResultCmd = 'GETRESULT %d %d\n' % (self.taskId, int(fullInfo))
        try:
            self.sock.sendall(getResultCmd)
        except socket.error: # remote server may has down
            self.sock.close()
            self.sock = None
            if not self.__connect():
                return False
            self.sock.sendall(getResultCmd)

        self.taskRsEvent.wait()
        self.taskRsEvent.clear()

        self.rsTimer.cancel()

        # timeout or error when return result
        if self.result == None:
            return False

        if fullInfo:
            return self.result
        else:
            if self.result.has_key('result'):
                return self.result['result']
            else:
                return False

    def __recvThreadFunc(self):
        """Thread function to receive data from task server."""
        while True:
            infds, outfds, errfds = select.select([self.sock,], [], [], 5)
            if infds and len(infds) == 0:
                if self.timeouted:
                    self.taskIdEvent.set()
                    self.taskRsEvent.set()
                    break
                else:
                    continue
            try:
                data = self.sock.recv(CFG_APP_BUFSIZ)
                if data == '':
                    self.taskIdEvent.set()
                    self.taskRsEvent.set()
                    break
            except:
                break
            self.aBuf.addData(data)
            cmdInfo = self.aBuf.getCommand()
            if cmdInfo:
                cmd, params = cmdInfo
                if cmd == 'TASKID':
                    try:
                        self.taskId = int(params)
                    except: 
                        self.taskId = None
                    self.taskIdEvent.set()
                elif cmd == 'RESULT':
                    resultInfo = params.split(None, 1) # (taskId, resultJson)
                    try:
                        resultJson = resultInfo[1]
                        self.result = cjson.decode(resultJson)
                    except:
                        self.result = None
                    self.taskRsEvent.set()
                break


def test():
    task = Task('127.0.0.1', 7708, 'testtype', 'testparams', 60, 'testapp')
    if not task.run():
        print 'Connect failed.'
    else:
        result = task.getResult(5)
        if not result:
            print 'No result return.'
        else:
            print result


if __name__ == '__main__':
    test()
